/**
 * @license
 * Copyright 2014 Google Inc. All rights reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview Provides the encryption panel for the prompt UI.
 */

goog.provide('e2e.ext.ui.panels.prompt.EncryptSign');

goog.forwardDeclare('e2e.ext.ui.Prompt');
goog.require('e2e.ext.constants.Actions');
goog.require('e2e.ext.constants.CssClass');
goog.require('e2e.ext.constants.ElementId');
goog.require('e2e.ext.ui.dialogs.Generic');
goog.require('e2e.ext.ui.dialogs.InputType');
goog.require('e2e.ext.ui.panels.Chip');
goog.require('e2e.ext.ui.panels.ChipHolder');
goog.require('e2e.ext.ui.panels.prompt.PanelBase');
goog.require('e2e.ext.ui.templates.panels.prompt');
goog.require('e2e.ext.utils.text');
goog.require('e2e.openpgp.asciiArmor');
goog.require('goog.Promise');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.classlist');
goog.require('goog.events.EventType');
goog.require('goog.object');
goog.require('goog.soy');
goog.require('goog.string');
goog.requireType('e2e.ext.Preferences');
goog.requireType('e2e.ext.actions.Executor');
goog.requireType('e2e.ext.messages.ApiRequest');
goog.requireType('e2e.ext.messages.BridgeMessageRequest');
goog.requireType('goog.events.Event');


goog.scope(function() {
var constants = e2e.ext.constants;
var dialogs = e2e.ext.ui.dialogs;
var messages = e2e.ext.messages;
var panels = e2e.ext.ui.panels;
var promptPanels = e2e.ext.ui.panels.prompt;
var templates = e2e.ext.ui.templates.panels.prompt;
var utils = e2e.ext.utils;


/**
 * Constructor for the encryption panel.
 */
promptPanels.EncryptSign = class extends promptPanels.PanelBase {
  /**
   * @param {!e2e.ext.actions.Executor} actionExecutor Executor for the
   *     End-to-End actions.
   * @param {!e2e.ext.messages.BridgeMessageRequest} content The content that
   *     the user wants to encrypt.
   * @param {!e2e.ext.Preferences} preferences User preferences.
   * @param {function(Error)} errorCallback A callback where errors will be
   *     passed to.
   */
  constructor(actionExecutor, content, preferences, errorCallback) {
    super(
        chrome.i18n.getMessage('promptEncryptSignTitle'), content,
        errorCallback);

    this.actionExecutor_ = actionExecutor;
    this.errorCallback_ = errorCallback;

    /**
     * A holder for the intended recipients of a PGP message.
     * @type {?panels.ChipHolder}
     * @private
     */
    this.chipHolder_ = null;

    /**
     * User preferences.
     * @type {e2e.ext.Preferences}
     * @private
     */
    this.preferences_ = preferences;
  }

  /** @override */
  decorateInternal(elem) {
    super.decorateInternal(elem);
    var content = this.getContent();
    var origin = content.origin;
    var signInsertLabel = this.canSend_(origin) ?
        chrome.i18n.getMessage('promptEncryptSignSendLabel') :
        chrome.i18n.getMessage('promptEncryptSignInsertLabel');

    goog.soy.renderElement(elem, templates.renderEncrypt, {
      insertCheckboxEnabled: content.canInject,
      canSaveDraft: content.canSaveDraft,
      signerCheckboxTitle: chrome.i18n.getMessage('promptSignMessageAs'),
      fromLabel: chrome.i18n.getMessage('promptFromLabel'),
      actionButtonTitle: chrome.i18n.getMessage('promptEncryptSignActionLabel'),
      cancelButtonTitle: chrome.i18n.getMessage('actionCancelPgpAction'),
      saveDraftButtonTitle:
          chrome.i18n.getMessage('promptEncryptSignSaveDraftLabel'),
      insertButtonTitle: signInsertLabel,
      subject: content.subject,
      subjectLabel: chrome.i18n.getMessage('promptSubjectLabel'),
      origin: content.origin
    });
  }

  /**
   * Checks if the web application in given origin supports sending the message
   * to recipients.
   * @param {string} origin The origin of the web application.
   * @return {boolean} True if the message can be sent.
   * @private
   */
  canSend_(origin) {
    return false;
  }

  /**
   * Populates the UI elements with the received data.
   * @private
   */
  populateUi_() {
    goog.Promise
        .all([
          // Populate the UI with the available encryption keys.
          this.renderEncryptionKeys_(),
          // Populate the UI with the available signing keys.
          this.renderSigningKeys_(),
          // Load selected content.
          this.loadSelectedContent_()
        ])
        .then(
            // When all of the above steps completed, set up focus.
            this.focusRelevantElement_, undefined, this);
  }

  /** @override */
  enterDocument() {
    super.enterDocument();
    var origin = this.getContent().origin;

    this.populateUi_();

    this.getHandler().listen(
        this.getElementByClass(constants.CssClass.ACTION),
        goog.events.EventType.CLICK, goog.bind(this.encryptSign_, this));

    if (this.getContent().canSaveDraft) {
      this.getHandler().listen(
          this.getElementByClass(constants.CssClass.SAVE),
          goog.events.EventType.CLICK,
          goog.partial(this.saveDraft_, origin, false));
    }

    if (this.getContent().canInject) {
      this.getHandler().listen(
          this.getElementByClass(constants.CssClass.INSERT),
          goog.events.EventType.CLICK,
          goog.partial(this.insertMessageIntoPage_, origin));
    }
  }

  /**
   * Renders the available encryption keys in the UI.
   * @return {!goog.Promise} Promise resolved when the encryption keys have
   *     been successfully rendered. It's never rejected.
   * @private
   */
  renderEncryptionKeys_() {
    return new goog.Promise(function(resolve, reject) {
      this.actionExecutor_.execute(
          /** @type {!e2e.ext.messages.ApiRequest} */ (
              {action: constants.Actions.LIST_KEYS, content: 'public'}),
          this, goog.bind(function(searchResult) {
            var providedRecipients = this.getContent().recipients || [];
            var intendedRecipients = [];
            var allAvailableRecipients = goog.object.getKeys(searchResult);
            var recipientsEmailMap =
                this.getRecipientsEmailMap_(allAvailableRecipients);
            goog.array.forEach(providedRecipients, function(recipient) {
              if (recipientsEmailMap.hasOwnProperty(recipient)) {
                goog.array.extend(
                    intendedRecipients, recipientsEmailMap[recipient]);
              }
            });
            this.chipHolder_ = new panels.ChipHolder(
                intendedRecipients, allAvailableRecipients,
                goog.bind(this.renderEncryptionPassphraseDialog_, this));
            this.addChild(this.chipHolder_, false);
            this.chipHolder_.decorate(
                goog.dom.getElement(constants.ElementId.CHIP_HOLDER));
            resolve();
          }, this), goog.bind(function(error) {
            this.chipHolder_ = new panels.ChipHolder(
                [], [],
                goog.bind(this.renderEncryptionPassphraseDialog_, this));
            this.addChild(this.chipHolder_, false);
            this.chipHolder_.decorate(
                goog.dom.getElement(constants.ElementId.CHIP_HOLDER));
            this.errorCallback_(error);
            resolve();
          }, this));
    }, this);
  }

  /**
   * Renders the available signing keys in the UI.
   * @return {!goog.Promise} Promise resolved when the signing keys have
   *     been successfully rendered. It's never rejected.
   * @private
   */
  renderSigningKeys_() {
    return new goog.Promise(function(resolve, reject) {
      this.actionExecutor_.execute(
          /** @type {!e2e.ext.messages.ApiRequest} */ (
              {action: constants.Actions.LIST_KEYS, content: 'private'}),
          this, goog.bind(function(privateKeyResult) {
            var availableSigningKeys = goog.object.getKeys(privateKeyResult);
            var signerSelect =
                goog.dom.getElement(constants.ElementId.SIGNER_SELECT);
            var signCheck =
                goog.dom.getElement(constants.ElementId.SIGN_MESSAGE_CHECK);

            if (availableSigningKeys.length == 0) {
              signCheck.disabled = true;
              signerSelect.disabled = true;
              var noKeysLabel = document.createTextNode(
                  chrome.i18n.getMessage('promptNoPrivateKeysFound'));
              var fromHolder =
                  goog.dom.getElement(constants.ElementId.FROM_HOLDER);
              fromHolder.appendChild(noKeysLabel);
            } else {
              signCheck.checked = true;
            }

            goog.array.forEach(availableSigningKeys, function(key) {
              var keyElem = document.createElement('option');
              keyElem.textContent = key;
              signerSelect.appendChild(keyElem);
            });
            resolve();
          }, this), goog.bind(function(error) {
            this.errorCallback_(error);
            resolve();
          }, this));
    }, this);
  }

  /**
   * Puts the focus on the chip holder (if no user chips are present) or the
   * textarea.
   * @private
   */
  focusRelevantElement_() {
    if (!this.getElement()) {
      return;
    }
    var textArea = /** @type {HTMLTextAreaElement} */
        (this.getElement().querySelector('textarea'));
    textArea.scrollTop = 0;
    textArea.setSelectionRange(0, 0);
    this.chipHolder_.focus();
    if (this.chipHolder_.hasChildren()) {
      // Double focus() workarounds a bug that prevents the caret from being
      // displayed in Chrome if setSelectionRange() is used.
      textArea.focus();
    }
  }

  /**
   * Extracts user addresses from user IDs and creates an email to user IDs map.
   * Ignores user IDs without a valid e-mail address.
   * @param  {!Array.<string>} recipients user IDs of recipients
   * @return {!Object.<string, !Array.<string>>} email to user IDs map
   * @private
   */
  getRecipientsEmailMap_(recipients) {
    var map = {};
    goog.array.forEach(recipients, function(recipient) {
      var email = utils.text.extractValidEmail(recipient);
      if (email) {
        if (!map.hasOwnProperty(email)) {
          map[email] = [];
        }
        map[email].push(recipient);
      }
    });
    return map;
  }

  /**
   * Renders the UI elements needed for requesting a passphrase for
   * symmetrically encrypting the current message.
   * @private
   */
  renderEncryptionPassphraseDialog_() {
    var passphraseDialog = new dialogs.Generic(
        chrome.i18n.getMessage('promptEncryptionPassphraseMessage'),
        goog.bind(
            function(passphrase) {
              goog.dispose(passphraseDialog);
              if (passphrase.length > 0) {
                this.renderEncryptionPassphraseConfirmDialog_(passphrase);
              }
            },
            this),
        dialogs.InputType.SECURE_TEXT, '',
        chrome.i18n.getMessage('actionEnterPassphrase'),
        chrome.i18n.getMessage('actionCancelPgpAction'));
    this.renderDialog(passphraseDialog);
  }

  /**
   * Renders the UI elements needed for requesting a passphrase for
   * symmetrically encrypting the current message.
   * @param {string} passphrase The original passphrase
   * @private
   */
  renderEncryptionPassphraseConfirmDialog_(passphrase) {
    var confirmDialog = new dialogs.Generic(
        chrome.i18n.getMessage('promptEncryptionPassphraseConfirmMessage'),
        goog.bind(
            function(confirmedPassphrase) {
              goog.dispose(confirmDialog);
              if (passphrase == confirmedPassphrase) {
                var chip = new panels.Chip(passphrase, true);
                this.chipHolder_.addChip(chip);
              } else {
                var errorDialog = new dialogs.Generic(
                    chrome.i18n.getMessage('keyMgmtPassphraseMismatchLabel'),
                    function() {
                      goog.dispose(errorDialog);
                    },
                    dialogs.InputType.NONE);
                this.renderDialog(errorDialog);
              }
            },
            this),
        dialogs.InputType.SECURE_TEXT, '',
        chrome.i18n.getMessage('actionEnterPassphrase'),
        chrome.i18n.getMessage('actionCancelPgpAction'));
    this.renderDialog(confirmDialog);
  }

  /**
   * Renders the original message to which the user wants to reply.
   * @param {HTMLTextAreaElement} textArea The text area where the reply body
   *     will be displayed.
   * @param {string} originalMsg The original message.
   * @private
   */
  renderReply_(textArea, originalMsg) {
    var replyLineToken = '\n> ';
    var replyBody =
        utils.text.prettyTextWrap(originalMsg, (78 - replyLineToken.length));
    textArea.value = goog.string.format(
        '\n\n%s:\n%s', chrome.i18n.getMessage('promptEncryptSignReplyHeader'),
        replyLineToken + replyBody.split('\n').join(replyLineToken));
    textArea.setSelectionRange(0, 0);
  }

  /**
   * Executes the ENCRYPT_SIGN action.
   * @private
   */
  encryptSign_() {
    var textArea = /** @type {HTMLTextAreaElement} */
        (this.getElement().querySelector('textarea'));
    var origin = this.getContent().origin;
    var request = /** @type {!e2e.ext.messages.ApiRequest} */ ({
      action: constants.Actions.ENCRYPT_SIGN,
      content: textArea.value,
      currentUser: goog.dom.getElement(constants.ElementId.SIGNER_SELECT).value
    });

    if (this.chipHolder_) {
      request.recipients = this.chipHolder_.getSelectedUids();
      request.encryptPassphrases = this.chipHolder_.getProvidedPassphrases();
    }

    var signerCheck =
        goog.dom.getElement(constants.ElementId.SIGN_MESSAGE_CHECK);
    request.signMessage = signerCheck && signerCheck.checked;

    this.actionExecutor_.execute(request, this, goog.bind(function(encrypted) {
      textArea.disabled = true;
      textArea.value = encrypted;
      this.chipHolder_.lock();
      var signCheckbox =
          goog.dom.getElement(constants.ElementId.SIGN_MESSAGE_CHECK);
      signCheckbox.disabled = true;
      this.renderDismiss();
      var insertButton = this.getElementByClass(constants.CssClass.INSERT);
      if (insertButton) {
        goog.dom.classlist.remove(insertButton, constants.CssClass.HIDDEN);
      }
    }, this));
  }

  /**
   * Inserts the encrypted content into the page.
   * @param {string} origin The web origin for which the PGP action is
   *     performed.
   * @private
   */
  insertMessageIntoPage_(origin) {
    var textArea = this.getElement().querySelector('textarea');
    var recipients = this.chipHolder_.getSelectedUids();
    var subject = goog.dom.getElement(constants.ElementId.SUBJECT) ?
        goog.dom.getElement(constants.ElementId.SUBJECT).value :
        undefined;
    var prompt = /** @type {e2e.ext.ui.Prompt} */ (this.getParent());
    var shouldSend = false;
    var content = this.getContent();
    if (content && this.canSend_(content.origin)) {
      shouldSend = true;
    }
    prompt.getHelperProxy().updateSelectedContent(
        textArea.value, recipients, origin, shouldSend,
        goog.partial(goog.dispose, this.getParent()),
        goog.bind(this.errorCallback_, this), subject);
  }

  /**
   * Loads the content that the user has selected in the web application.
   * @return {!goog.Promise} Promise resolved when content has been loaded.
   *     It's never rejected.
   * @private
   */
  loadSelectedContent_() {
    return new goog.Promise(function(resolve, reject) {
      var origin = this.getContent().origin;
      var textArea = /** @type {HTMLTextAreaElement} */
          (this.getElement().querySelector('textarea'));
      var content = this.getContent().selection || '';
      var detectedAction = utils.text.getPgpAction(content);
      if (detectedAction == constants.Actions.DECRYPT_VERIFY) {
        this.actionExecutor_.execute(
            /** @type {!e2e.ext.messages.ApiRequest} */ ({
              action: constants.Actions.DECRYPT_VERIFY,
              content: content,
              passphraseCallback: goog.bind(this.renderPassphraseDialog, this)
            }),
            this, goog.bind(function(decrypted) {
              if (e2e.openpgp.asciiArmor.isDraft(content)) {
                textArea.value = decrypted;
              } else {
                this.renderReply_(textArea, decrypted);
              }
              resolve();
            }, this), goog.bind(function(error) {
              this.errorCallback_(error);
              resolve();
            }, this));
      } else {
        resolve();
      }
    }, this);
  }

  /**
   * Encrypts the current draft and persists it into the web application that
   * the user is interacting with.
   * @param {string} origin The web origin where the message was created.
   * @param {goog.events.Event} evt The event that triggers the saving of the
   *     draft.
   * @private
   */
  saveDraft_(origin, evt) {
    var formText = /** @type {HTMLTextAreaElement} */
        (this.getElement().querySelector('textarea'));
    var subject = goog.dom.getElement(constants.ElementId.SUBJECT) ?
        goog.dom.getElement(constants.ElementId.SUBJECT).value :
        undefined;
    var signer = goog.dom.getElement(constants.ElementId.SIGNER_SELECT).value;
    this.actionExecutor_.execute(
        /** @type {!e2e.ext.messages.ApiRequest} */ ({
          action: constants.Actions.ENCRYPT_SIGN,
          content: formText.value,
          recipients: [signer],
          currentUser: signer
        }),
        this, goog.bind(function(encrypted) {
          var draft = e2e.openpgp.asciiArmor.markAsDraft(encrypted);
          // Inject the draft into website.
          var prompt = /** @type {e2e.ext.ui.Prompt} */ (this.getParent());
          var shouldSend = false;
          prompt.getHelperProxy().updateSelectedContent(
              draft, [], origin, shouldSend, goog.nullFunction,
              goog.bind(this.errorCallback_, this), subject);
        }, this), goog.bind(function(error) {
          if (error.messageId == 'promptNoEncryptionTarget') {
            var dialog = new dialogs.Generic(
                chrome.i18n.getMessage('promptNoEncryptionKeysFound'),
                function() {
                  goog.dispose(dialog);
                },
                dialogs.InputType.NONE);
            this.renderDialog(dialog);
          }

          // NOTE(radi): Errors are silenced here on purpose.
        }, this));
  }
};


});  // goog.scope
